﻿using Nemerle.Collections;
using Nemerle.Text;
using Nemerle.Utility;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;

using System;
using System.Collections.Generic;
using System.Linq;

using NRails.Migrations;
using BLToolkit.Data.Sql;

namespace NRails.Macros.Migration
{
  module MigrationImpl
  {
      public Create(_ : Typer, name : PExpr, code : PExpr) : PExpr
      {
          def t = <[ $("t" : usesite) ]>;
          def c = <[ $("c" : usesite) ]>;
          
          <[
            this.CreateTable($(name.ToString() : string), $t => 
            {
                $(CompileColumns(t, c, code, false))
            });
          ]>
      }

      public Change(_ : Typer, name : PExpr, code : PExpr) : PExpr
      {
          // todo: поправить CompileColumn, для изменения существующего поля
          // возможность не указывать тип поля, например:
          // Id : same(len = 20)
          def c = <[ $("c" : usesite) ]>;
          def t = <[ $("t" : usesite) ]>;
          
          
          <[
            this.ChangeTable($(name.ToString() : string), $t => 
            {
                $(CompileColumns(t, c, code, true))
            });
          ]>
      }
      
      variant ParsedAction
      {
          | Create
          | Change
      }
      
      CompileColumns(t : PExpr.Ref, c : PExpr.Ref, code : PExpr, allowChanges : bool) : PExpr
      {
          mutable colNames = Set.[string]();
          
          def checkChangesMode(loc : Location, allowedResult : void -> PExpr)
          {
              if (allowChanges)
                allowedResult()
              else
              {
                Message.Error(loc, "No changes allowed here.");
                <[]>
              }
          }
          
          def newField(name, action) 
          {
              if (colNames.Contains(name.ToString()))
              {
                  Message.Error(name.Location, $"Duplicate column '$name'");
                  <[]>
              }
              else
              {
                  colNames = colNames.Add(name.ToString());
                  action();
              }
          }
          
          def compile(s : PExpr, action) : PExpr
          {
            match (s)          
            {
                | PExpr.MacroCall(name, _, expr) =>
                    match (name.ToString(), expr)
                    {
                        | ("change", [SyntaxElement.Expression(x)]) => 
                            checkChangesMode(s.Location, () => compile(x, ParsedAction.Change()));

                        | ("rename", [SyntaxElement.Expression(oldName), SyntaxElement.Expression(newName)]) => 
                            checkChangesMode(s.Location, () => 
                                match (oldName, newName)
                                {
                                    | (PExpr.Ref(_), PExpr.Ref(_)) =>
                                       <[ $t.Rename($(oldName.ToString() : string), $(newName.ToString() : string)) ]>
                                    | _ => Message.Error(name.Location, "Invalid drop syntax"); <[]>
                                });

                        | ("drop", [SyntaxElement.Expression(x)]) => 
                            checkChangesMode(s.Location, () => 
                                match (x)
                                {
                                    | PExpr.Ref(name) =>
                                       <[ $t.Drop($(name.ToString() : string)) ]>
                                    | _ => Message.Error(x.Location, "Invalid drop syntax"); <[]>
                                });
                        |  ("createindex", [SyntaxElement.Expression(expr)]) =>
                            CompileCreateIndex(t, expr);
                        |  ("dropindex", [SyntaxElement.Expression(expr)]) =>
                            CompileDropIndex(t, expr);
                        | (_, _) => 
                           Message.Error(s.Location, $"Invalid macro call '$(name.ToString())'");
                           <[]>
                    }
                | <[ $name :> $type_? ]> with isNullable = true
                | <[ $name :> $type_ ]> with isNullable = false =>
                  newField(name, () => CompileReference(t, c, name, type_, isNullable, action));
                  
                
                | <[ $name : $type_?; ]> with (defExpr = null, isNullable = true)
                | <[ $name : $type_; ]> with (defExpr = null, isNullable = false)
                | <[ $name : $type_? = $defExpr; ]> with isNullable = true
                | <[ $name : $type_ = $defExpr; ]> with isNullable = false => 
                  newField(name, () => CompileColumn(t, c, name, type_, defExpr, isNullable, action));

                | x => 
                    Message.Error(x.Location, $"Ivalid field definition $x, $(x.GetType())");
                    <[]>;
            }
          }
          match (code)
          {
              | PExpr.Sequence(seq) => PExpr.Sequence(seq.Map(compile(_, ParsedAction.Create())))
              | x => compile(x, ParsedAction.Create());
          }
      }

      CompileCreateIndex(t : PExpr.Ref, expr : PExpr) : PExpr
      {
          def parseField(f)
          {
              match (f)
              {
                  | <[ $(id : name)(asc) ]> 
                  | <[ $(id : name) ]> => id.ToString();
                  | <[ $(id : name)(desc) ]> => $"$(id.ToString()) DESC";
                  | _ => Message.Error(f.Location, "Invalid field, syntax: FieldName[(desc)]"); String.Empty;
              }
          }
          match (expr)
          {
              | <[ $(indexName : name)(..$fields) ]> =>
                def fieldsDef = String.Join(", ", fields.Map(parseField).ToArray());
                <[ $t.CreateIndex($(indexName.ToString() : string), $(fieldsDef : string)); ]>
              | _ => 
                Message.Error(expr.Location, "Invalid index definition, valid syntax: createindex IndexName(field1, field2 [desc])");
                <[]>
          }
      }
      
      CompileDropIndex(t : PExpr.Ref, expr : PExpr) : PExpr
      {
          match (expr)
          {
              | <[ $(indexName : name) ]> =>
                <[ $t.DropIndex($(indexName.ToString() : string)); ]>
              | _ => 
                Message.Error(expr.Location, "Invalid dropindex syntax, valid: dropindex IndexName");
                <[]>
          }
      }
      
      CompileReference(t : PExpr.Ref, _ : PExpr.Ref, name : PExpr, type_ : PExpr, isNullable : bool, action : ParsedAction) : PExpr
      {
          match (action)
          {
              | ParsedAction.Create =>
                  match (type_)
                  {
                      | <[ $type_(pk)]> =>
                        <[ 
                            $t.AddPk($(name.ToString() : string)); 
                            $t.CreateReference($(name.ToString() : string), $(type_.ToString() : string), $(isNullable : bool));
                         ]>
                      | _ => 
                        <[ 
                            $t.CreateReference($(name.ToString() : string), $(type_.ToString() : string), $(isNullable : bool));
                         ]>
                  }
              
              | _ => 
                Message.Error(type_.Location, "Only create references allowed");
                <[]>
          }
      }
      
      mutable SqlDataTypeConsts : IDictionary[string, Reflection.FieldInfo];
      ParseType(name : string) : SqlDataType
      {
          match (name.ToLowerInvariant())
          {
            | "bool" => SqlDataType.Boolean;
            | "byte[]" => SqlDataType.ByteArray;
            | name => 
            {
              when (SqlDataTypeConsts == null)
              {
                SqlDataTypeConsts = typeof(SqlDataType)
                    .GetFields(BindingFlags.Static | BindingFlags.Public)
                    .Filter(f => typeof(SqlDataType).Equals(f.FieldType))
                    .ToDictionary(f => f.Name.ToLowerInvariant());
              }
              
              def findField(name)
              {
                  if (SqlDataTypeConsts.ContainsKey(name))
                      SqlDataTypeConsts[name];
                  else
                      null
              }
              
              def fieldInfo = [name, $"db$name"].Map(n => findField(n)).Find(_ != null);
              
              match (fieldInfo)
              {
                  | Some(fi) => fi.GetValue(null) :> SqlDataType;
                  | None => null
              }
            }
          }
      }
      
      CompileColumn(t : PExpr.Ref, c : PExpr.Ref, name : PExpr, type_ : PExpr, defExpr : PExpr, isNullable : bool, action : ParsedAction) : PExpr
      {
          mutable body = [<[ $c.Name = $(name.ToString() : string);]>];
          mutable pkAction = [<[]>];
          
          def Add(p)   { body = p :: body;}
          def AddPk(p) { pkAction = p :: pkAction;}
          
          
          match (type_)
          {
            | <[ $typeName(..$parms) ]> 
            | <[ $typeName ]> with parms = [] => 
                def strType = typeName.ToString();
                
                def columnType = ParseType(strType);
                
                if (columnType != null)
                {
                    mutable size = columnType.Length;
                    mutable scale = -1; //columnType.Scale;
                    mutable precision = -1; //columnType.Precision;
                    
                    def compileParm(parm : PExpr)
                    {
                      match (parm)
                      {
                          | <[ Scale = $(number : int)]>
                          | <[ scale = $(number : int)]> 
                            => 
                            when (size > 0) Message.Error(parm.Location, "Can not define scale with size.");
                            scale = number;
                            
                          | <[ Precision = $(number : int)]>
                          | <[ precision = $(number : int)]> 
                            => 
                            when (size > 0) Message.Error(parm.Location, "Can not define precision with size.");
                            precision = number;
                          
                          | <[ Size = $(number : int)]> 
                          | <[ len = $(number : int)]> 
                          | <[ length = $(number : int)]> 
                          | <[ size = $(number : int)]> 
                          | <[ $(number : int)]> 
                            =>
                            when (precision > 0 || scale > 0) Message.Error(parm.Location, "Can not define size with precision and scale.");
                            size = number;

                          | <[ pk ]> 
                            => AddPk(<[ $t.AddPk($(name.ToString() : string)); ]>);
                          
                          | <[ identity]> with (seed = 1, increment = 1) 
                          | <[ identity($(seed : int), $(increment : int))]> 
                            => 
                              Add(<[ $c.AutoIncrement = true; ]>);
                              Add(<[ $c.Seed = $(seed : int); ]>);
                              Add(<[ $c.Increment = $(increment : int); ]>);
                                  
                          | <[ $x = $_ ]> 
                          | x => Message.Error(x.Location, $"Invalid param '$x'")
                      }
                    }
                    
                    parms.Iter(compileParm);
                    
                    def sqlDbType = <[System.Data.SqlDbType.$(columnType.DbType.ToString() : usesite)]>;
                
                    def typeExpr = match (size, precision, scale)
                    {
                        | (size, _, _) when size > 0 
                            =>
                             <[ $c.Type = BLToolkit.Data.Sql.SqlDataType( $sqlDbType, $(size : int)); ]>;
                        | (_, _, _) when precision <= 0
                            =>
                             <[ $c.Type = BLToolkit.Data.Sql.SqlDataType( $sqlDbType); ]>;
                        | (_, precision, scale) 
                            =>
                             <[ $c.Type = BLToolkit.Data.Sql.SqlDataType( $sqlDbType, $(precision : int), $(scale : int)); ]>;
                    }
                    Add(typeExpr);
                    
                    Add(<[ $c.Nullable = $(isNullable : bool); ]>);
                }
                else
                {
                    Message.Error(typeName.Location, $"Invalid type name '$strType'");
                }
          }
          
          match (defExpr)
          {
              | null => ()
              // todo: надо разобраться как вычислять выражения типа даты
              // и преобразовывать их в нужный сиквельный эквивалент (похоже тулкит умеет, глядел мельком)
              | <[ $(x : int) ]> => Add(<[ $c.DefaultValue = $("'"  + x.ToString() + "'": string) ; ]>);
              | <[ $(x : string) ]> => Add(<[ $c.DefaultValue = $("'"  + x + "'": string); ]>);
              | <[ $x ]> => Message.Error(x.Location, "Only string or int default value supported.");
          }
          
          match (action)
          {
              | ParsedAction.Change() =>
                  <[ 
                      $t.Change($(name.ToString() : string), $c => {
                          ..$body
                      });
                      {..$pkAction};
                  ]>;
              | _ =>
                  <[ 
                      $t.Add($c => {
                          ..$body
                      });
                      {..$pkAction};
                  ]>;
          }
      }
  }
}
